Day 02 的 Deployment 已經解決一件事:todo-api 少了一個副本時,Kubernetes 會建立新的 Pod 補回來。不過 Pod 不是固定不動的主機。它重建後 IP 可能改變,也可能還沒準備好處理請求。若前端直接記住某個 Pod IP,下一次重建就會失效。
我們把 Todo 系統簡化成兩個對外提供功能的元件:web 負責網頁,todo-api 提供 API。今天這一篇用它們的 manifest 看三種資源各自負責哪一段;先把服務如何被找到、外部流量如何進入叢集說清楚。明天再來處理同一組設定要怎麼套用到不同環境。
使用者開啟 http://todo.localhost/api/todos 時,請求的路徑如下:
Browser
-> NGINX Ingress Controller
-> todo Ingress
-> todo-api Service
-> todo-api Pod
瀏覽器不會直接連到 Pod。Ingress 根據 host 和 path 選擇後端 Service;Service 再將流量導向符合 label 的可用 Pod。Deployment 則負責讓這些 Pod 維持在指定數量。這三者的責任不同,但名稱、label 和 port 必須彼此對得上。
下列依舊是 todo-api 的 Deployment 的範例。先前已經說明 Deployment 與 ReplicaSet 如何維持副本數;這裡要看 Service 之後會用到的 label,以及決定 Pod 能否接流量的 probe。
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-api
namespace: todo
spec:
replicas: 2
selector:
matchLabels:
app: todo-api
template:
metadata:
labels:
app: todo-api
spec:
containers:
- name: todo-api
image: todo-api:0.1.0
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /health/ready
port: http
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 10
readinessProbe 成功前,Pod 會是 NotReady。Service controller 建立的 EndpointSlice 不會把它列為可接收流量的 endpoint,因此剛啟動、尚未完成初始化的 API 不會立刻收到請求。
livenessProbe 處理的是另一件事。它持續失敗時,kubelet 會重啟 container。兩個 probe 都指向 HTTP endpoint,但分開設定,讓應用程式可以區分「暫時不接流量」和「process 已經需要重啟」。
Pod IP 是會變動的,而 Service 的名稱不會。以下 Service 案例指出,選取 app: todo-api 的 Pod,並在 todo Namespace 中提供 todo-api 這個名稱。
apiVersion: v1
kind: Service
metadata:
name: todo-api
namespace: todo
spec:
selector:
app: todo-api
ports:
- name: http
port: 8080
targetPort: http
這個 Service 對外接收 8080 port 的流量,再交給 container 的 http port。http 是前面 containerPort: 8080 設定的名稱,所以兩者最後都會連到 API 的 8080 port。也可以直接把 targetPort 寫成 8080;這裡使用名稱,是為了讓 Service 和 container 的對應關係更清楚。
最常見的問題也在這裡:Service 建立成功,不代表後面真的有 Pod。selector 若和 Pod label 不一致,或 Pod 還沒 ready,EndpointSlice 就不會有可轉送的位址。此時重新部署通常沒有幫助,先檢查 label 和 probe 的結果比較快。
web 也用相同模式建立自己的 Deployment 和 Service,只是它監聽 80 port,label 是 app: web。
Ingress 只是一份 HTTP 路由規則,實際接收流量的是 Ingress Controller。本範例使用 NGINX Ingress Controller,所以 ingressClassName 設為 nginx;如果叢集沒有安裝相對應的 controller,Ingress 資源存在也不會讓 todo.localhost 開始服務。
這份 Ingress 將 todo.localhost 的 /api 導向 API,其他路徑導向網頁:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: todo
namespace: todo
spec:
ingressClassName: nginx
rules:
- host: todo.localhost
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: todo-api
port:
name: http
- path: /
pathType: Prefix
backend:
service:
name: web
port:
name: http
pathType: Prefix 讓 /api/todos 也符合 /api 這條規則。/ 是另一條後備規則,因此瀏覽器請求首頁時會進到 web Service。Ingress 不知道 Pod IP,也不需要知道;它只需要後端 Service 的名稱與 port。
一開始先維持這些基本規則就夠了。TLS、認證、rate limit 和其他 NGINX annotation 都有各自的用途,但把它們一次塞進第一個範例,只會讓路由關係更難看清楚。
這裡展示外部請求經 NGINX Ingress Controller、Ingress、Service 到 Ready Pod 的流向與其對應關係。

完成本機 image 建置與載入後:
kubectl rollout status deployment/todo-api -n todo
kubectl rollout status deployment/web -n todo
kubectl get deployment,service,ingress -n todo
kubectl get endpointslice -n todo -l kubernetes.io/service-name=todo-api
todo-api 的 EndpointSlice 應列出 ready 的 API Pod。若沒有 endpoint,先查看 Pod 的狀態和事件:
kubectl get pods -n todo -l app=todo-api
kubectl describe pod <todo-api-pod-name> -n todo
叢集尚未安裝 NGINX Ingress Controller 時,仍可直接透過 Service 驗證 API (假設環境為 KinD):
kubectl port-forward service/todo-api 8080:8080 -n todo
curl http://127.0.0.1:8080/api/todos
這組資源可以使用本機 image,也可以改為從 container registry 拉取 image;兩種情況的資源關係相同。下一篇會從這份固定的 YAML 出發,整理哪些設定應該共用,哪些設定應隨 Dev、Staging 和 Prod 改變。